package org.hackystat.devcathlon.engine.event.impl;

import java.util.HashMap;
import javax.xml.datatype.XMLGregorianCalendar;
import org.hackystat.dailyprojectdata.resource.devtime.DevTimeCounter;
import org.hackystat.devcathlon.engine.event.Event;
import org.hackystat.devcathlon.engine.event.EventException;
import org.hackystat.devcathlon.engine.event.EventScore;
import org.hackystat.devcathlon.engine.match.Match;
import org.hackystat.devcathlon.engine.team.Team;
import org.hackystat.devcathlon.engine.user.User;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.hackystat.utilities.logger.HackystatLogger;
import org.hackystat.utilities.tstamp.Tstamp;

/**
 * Reward developers for ensuring that most files are worked on by multiple developers.
 * 
 * Rational: Want to encourage developers to work together on files. However, we do not want to
 * award points when little development effort is being performed on the system.
 * 
 * <p>
 * Requires the following properties in the configuration.xml file for this event:
 * <ul>
 * <li>WakeupInterval: How frequently this event is invoked in minutes.
 * <li>DevTimeMinutes: Minimum dev time minutes needed for points.
 * <li>FilesPercentage: Minimum percentage of files that were co-developed needed for points.
 * <li>MinNumDevelopers: Minimum number of developers that co-developed files for points.
 * <li>AwardPoints: Points awarded for meeting DevTimeMinutes, FilesPercentage and MinNumDevelopers.
 * <li>BonusDevTimeMinutes: Minimum dev time minutes needed for bonus points.
 * <li>BonusFilesPercentage: Minimum percentage of files that were co-developed needed for bonus
 * points.
 * <li>BonusMinNumDevelopers: Minimum number of developers that co-developed files for bonus points.
 * <li>BonusAwardPoints: Bonus points awarded for meeting BonusDevTimeMinutes, BonusFilesPercentage
 * and BonusMinNumDevelopers.
 * </ul>
 * <p>
 * 
 * Specification:
 * <ul>
 * <li>Wakeup every WakeupInterval.
 * <li>Gather DevEvent data to construct/update a map of all files that are being worked on, which
 * developers have worked on which files, and how much DevTime they have so far contributed to each
 * file. If a developer has contributed at least DevTime to a particular file, then their
 * contribution is recognized, otherwise it is not.
 * <li>Award the team AwardPoints if Percent of all files have been worked on by at least
 * NumDevelopers developers.
 * <li>Base Score: BonusAward points if BonusPercent of all files have been worked on by at least
 * BonusNumDevelopers. Do not award any points for a given day if less than BonusNumDevelopers
 * developers have worked on that day and contributed less than BonusDevTime time.
 * </ul>
 * 
 * @author Anthony Du, John Ancheta
 * 
 */
public class CollectiveOwnership extends Event {

  /** Support serialization. */
  private static final long serialVersionUID = 1L;

  /** The unique name for this event. */
  public static final String eventName = "Collective Ownership";

  /** The unique description for this event. */
  public static final String description = "Reward developers for ensuring that most files are "
      + "worked on by multiple developers.";

  /** The name of the bonus award. */
  public static final String bonusName = "CollectiveOwnershipBonusAward";

  /** Dev Event string for SDT. */
  public static final String devEvent = "DevEvent";

  /** Minutes needed for points. */
  private int devTimeMinutes;

  /** DevTimeMinutes key. */
  public static final String devTimeMinutesKey = "DevTimeMinutes";

  /** Points awarded. */
  private int awardPoints;

  /** AwardPointsKey key. */
  public static final String awardPointsKey = "AwardPoints";

  /** Percentage of files that should be worked on. */
  private int filesPercentage;

  /** FilesPercentage key. */
  public static final String filesPercentageKey = "FilesPercentage";

  /** Number of developers that should be working on the files. */
  private int minNumDevelopers;

  /** MinNumDevelopers key. */
  public static final String minNumDevelopersKey = "MinNumDevelopers";

  /** Minutes needed for points. */
  private int bonusDevTimeMinutes;

  /** BonusDevTimeMinutes key. */
  public static final String bonusDevTimeMinutesKey = "BonusDevTimeMinutes";

  /** Points awarded. */
  private int bonusAwardPoints;

  /** BonusAwardPoints key. */
  public static final String bonusAwardPointsKey = "BonusAwardPoints";

  /** Percentage of files that should be worked on. */
  private int bonusFilesPercentage;

  /** BonusFilesPercentage key. */
  public static final String bonusFilesPercentageKey = "BonusFilesPercentage";

  /** Number of developers that should be working on the files. */
  private int bonusMinNumDevelopers;

  /** BonusMinNumDevelopers key. */
  public static final String bonusMinNumDevelopersKey = "BonusMinNumDevelopers";

  /**
   * Creates a new event for Collective Ownership. Ensures all required properties are available.
   * 
   * @throws EventException If the configuration cannot be created.
   */
  public CollectiveOwnership() throws EventException {
    super(eventName, description);
    try {
      devTimeMinutes = Integer.parseInt(configManager.getProperty(eventName, devTimeMinutesKey));
      awardPoints = Integer.parseInt(configManager.getProperty(eventName, awardPointsKey));
      filesPercentage = Integer.parseInt(configManager.getProperty(eventName, filesPercentageKey));
      minNumDevelopers = Integer
          .parseInt(configManager.getProperty(eventName, minNumDevelopersKey));
      bonusDevTimeMinutes = Integer.parseInt(configManager.getProperty(eventName,
          bonusDevTimeMinutesKey));
      bonusAwardPoints = Integer
          .parseInt(configManager.getProperty(eventName, bonusAwardPointsKey));
      bonusFilesPercentage = Integer.parseInt(configManager.getProperty(eventName,
          bonusFilesPercentageKey));
      bonusMinNumDevelopers = Integer.parseInt(configManager.getProperty(eventName,
          bonusMinNumDevelopersKey));
    }
    catch (Exception e) {
      throw new EventException("Error configuring properties", e);
    }
  }

  /**
   * Returns devTimeMinutes.
   * 
   * @return devTimeMinutes.
   */
  public int getDevTimeMinutes() {
    return devTimeMinutes;
  }

  /**
   * Returns awardPoints.
   * 
   * @return awardPoints.
   */
  public int getAwardPoints() {
    return awardPoints;
  }

  /**
   * Returns filesPercentage.
   * 
   * @return filesPercentage.
   */
  public int getFilesPercentage() {
    return filesPercentage;
  }

  /**
   * Returns minNumDevelopers.
   * 
   * @return minNumDevelopers.
   */
  public int getMinNumDevelopers() {
    return minNumDevelopers;
  }

  /**
   * Returns bonusDevTimeMinutes.
   * 
   * @return bonusDevTimeMinutes.
   */
  public int getBonusDevTimeMinutes() {
    return bonusDevTimeMinutes;
  }

  /**
   * Returns bonusAwardPoints.
   * 
   * @return bonusAwardPoints.
   */
  public int getBonusAwardPoints() {
    return bonusAwardPoints;
  }

  /**
   * Returns bonusFilesPercentage.
   * 
   * @return bonusFilesPercentage.
   */
  public int getBonusFilesPercentage() {
    return bonusFilesPercentage;
  }

  /**
   * Returns bonusMinNumDevelopers.
   * 
   * @return bonusMinNumDevelopers.
   */
  public int getBonusMinNumDevelopers() {
    return bonusMinNumDevelopers;
  }

  /**
   * Returns wakeupInterval.
   * 
   * @return wakeupInterval.
   */
  public int getWakeupInterval() {
    return wakeupInterval;
  }

  /**
   * Returns the total number of points for this event.
   * 
   * The total is the sum of base score plus the bonus score.
   * 
   * @param score The event score.
   * @return The total points for this event given the current weights.
   */
  public int computePoints(EventScore score) {
    return score.getBaseScore() + score.getBonusScore(bonusName);
  }

  /**
   * Creates an EventScore instance for CollectiveOwnership.
   * 
   * @param match The match of interest.
   * @param team The team of interest.
   * @param timestamp the time at which this event is being run.
   * @return The EventScore.
   * @throws EventException If problems occur while processing this event.
   */
  public EventScore computeScore(Match match, Team team, XMLGregorianCalendar timestamp)
      throws EventException {
    // create the default score, which awards no points.
    EventScore score = new EventScore(timestamp, match, team);
    // score explanation
    String explanation = "Collective ownership for ";
    // find the project and the sensorbase client.
    Project project = match.getProject(team);
    SensorBaseClient client = match.getSensorBaseClient(team);
    // get the Build sensor data for the previous interval of time.
    String projectName = project.getName();
    String owner = project.getOwner();
    String sdt = devEvent; // set Sensor Data Type to "DevEvent"
    XMLGregorianCalendar endTime = timestamp;
    XMLGregorianCalendar startTime = Tstamp.incrementMinutes(endTime, wakeupInterval * -1);
    SensorDataIndex index;
    HackystatLogger.getLogger("org.hackystat.devcathlon", "devcathlon").info(
        String.format("Getting DevEvent sensor data for match %s and team %s", match.getName(),
            team.getName()));
    try {
      index = client.getProjectSensorData(owner, projectName, startTime, endTime, sdt);
    }
    catch (Exception e) {
      throw new EventException("Error retrieving build data.", e);
    }

    // User Dev Time Map containing a HashMap pair of (user, dev time).
    HashMap<String, Integer> userDevTimeMap = new HashMap<String, Integer>();
    try {
      // set the keys
      for (SensorDataRef ref : index.getSensorDataRef()) {
        SensorData data = client.getSensorData(ref);
        String filename = data.getResource();
        if (!userDevTimeMap.containsKey(filename)) {
          userDevTimeMap.put(filename, 0);
        }
      }
    }
    catch (Exception e) {
      throw new EventException("Error building data.", e);
    }

    setUserDataMap(userDevTimeMap, team, projectName, client, index, devTimeMinutes);
    if (isQualifiedForAward(userDevTimeMap, filesPercentage, minNumDevelopers)) {
      score.setBaseScore(awardPoints);
      explanation = "Awarded Collective Ownership for " 
                    + filesPercentage + "% files co-developed by " 
                    + minNumDevelopers + " co-developers.";
      score.setExplanation(explanation);
    }
    else {
      score.setExplanation("This project lacks collaboration amongst developers.");
    }

    // redo map only if regular devTimeMinutes differs from bonus.
    if (devTimeMinutes != bonusDevTimeMinutes) {
      setUserDataMap(userDevTimeMap, team, projectName, client, index, bonusDevTimeMinutes);
    }
    
    if (isQualifiedForAward(userDevTimeMap, bonusFilesPercentage, bonusMinNumDevelopers)) {
      score.addBonusScore(bonusName, bonusAwardPoints);
      explanation = "Awarded Collective ownership with bonus points for " 
                    + bonusFilesPercentage + "% files co-developed by " 
                    + bonusMinNumDevelopers + " co-developers.";
      score.setExplanation(explanation);
    }

    // Finally return the final score.
    return score;
  }

  /**
   * Computes percentage of files that have been worked on.
   * 
   * @param userDevTimeMap User time map;
   * @param awardPercentage Required percentage needed for award.
   * @param developers Minimum required number of developers needed for award.
   * @return true if qualified, else false.
   */
  private boolean isQualifiedForAward(HashMap<String, Integer> userDevTimeMap, int awardPercentage,
      int developers) {
    int count = 0;
    for (Integer value : userDevTimeMap.values()) {
      if (value.intValue() >= developers) {
        count++;
      }
    }
    double size = userDevTimeMap.size();
    double percentage = (count / size) * 100;
    return percentage >= awardPercentage;
  }

  /**
   * Sets the User Data Map given a sensor data type of "DevEvent" from an index of sensor base
   * instances, or null if none was found.
   * 
   * @param userDevTimeMap User time map;
   * @param team The project team.
   * @param project The project name.
   * @param client The sensorbase client.
   * @param index The SensorDataIndex.
   * @param minutes Required minutes for award.
   * @throws EventException If problems occur while processing this event.
   */
  private void setUserDataMap(HashMap<String, Integer> userDevTimeMap, Team team, String project,
      SensorBaseClient client, SensorDataIndex index, int minutes) throws EventException {
    try {
      // fill the map
      for (String key : userDevTimeMap.keySet()) {
        int credits = 0;
        for (User user : team.getMembers()) {
          String email = user.getEmail();
          DevTimeCounter devTimeCounter = new DevTimeCounter();
          for (SensorDataRef ref : index.getSensorDataRef()) {
            SensorData data = client.getSensorData(ref);
            String resource = data.getResource();
            String owner = data.getOwner();
            if (key.equals(resource) && email.equals(owner)) {
              devTimeCounter.addDevEvent(data.getTimestamp());
            }
          }
          if (devTimeCounter.getDevTime().intValue() >= minutes) {
            credits++;
          }
        }
        userDevTimeMap.put(key, credits);
      }
    }
    catch (Exception e) {
      throw new EventException("Error in reading DevEvents", e);
    }
  }
}
